<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Cache
 * @subpackage Zend_Cache_Backend
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id: Memcached.php 18951 2009-11-12 16:26:19Z alexander $
 */


/**
 * @see Zend_Cache_Backend_Interface
 */
require_once 'Zend/Cache/Backend/ExtendedInterface.php';

/**
 * @see Zend_Cache_Backend
 */
require_once 'Zend/Cache/Backend.php';


/**
 * @package    Zend_Cache
 * @subpackage Zend_Cache_Backend
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Cache_Backend_Memcached extends Zend_Cache_Backend implements Zend_Cache_Backend_ExtendedInterface
{
	/**
	 * Default Values
	 */
	const DEFAULT_HOST = '127.0.0.1';
	const DEFAULT_PORT =  11211;
	const DEFAULT_PERSISTENT = true;
	const DEFAULT_WEIGHT  = 1;
	const DEFAULT_TIMEOUT = 1;
	const DEFAULT_RETRY_INTERVAL = 15;
	const DEFAULT_STATUS = true;
	const DEFAULT_FAILURE_CALLBACK = null;

	/**
	 * Log message
	 */
	const TAGS_UNSUPPORTED_BY_CLEAN_OF_MEMCACHED_BACKEND = 'Zend_Cache_Backend_Memcached::clean() : tags are unsupported by the Memcached backend';
	const TAGS_UNSUPPORTED_BY_SAVE_OF_MEMCACHED_BACKEND =  'Zend_Cache_Backend_Memcached::save() : tags are unsupported by the Memcached backend';

	/**
	 * Available options
	 *
	 * =====> (array) servers :
	 * an array of memcached server ; each memcached server is described by an associative array :
	 * 'host' => (string) : the name of the memcached server
	 * 'port' => (int) : the port of the memcached server
	 * 'persistent' => (bool) : use or not persistent connections to this memcached server
	 * 'weight' => (int) : number of buckets to create for this server which in turn control its
	 *                     probability of it being selected. The probability is relative to the total
	 *                     weight of all servers.
	 * 'timeout' => (int) : value in seconds which will be used for connecting to the daemon. Think twice
	 *                      before changing the default value of 1 second - you can lose all the
	 *                      advantages of caching if your connection is too slow.
	 * 'retry_interval' => (int) : controls how often a failed server will be retried, the default value
	 *                             is 15 seconds. Setting this parameter to -1 disables automatic retry.
	 * 'status' => (bool) : controls if the server should be flagged as online.
	 * 'failure_callback' => (callback) : Allows the user to specify a callback function to run upon
	 *                                    encountering an error. The callback is run before failover
	 *                                    is attempted. The function takes two parameters, the hostname
	 *                                    and port of the failed server.
	 *
	 * =====> (boolean) compression :
	 * true if you want to use on-the-fly compression
	 *
	 * =====> (boolean) compatibility :
	 * true if you use old memcache server or extension
	 *
	 * @var array available options
	 */
	protected $_options = array(
        'servers' => array(array(
            'host' => self::DEFAULT_HOST,
            'port' => self::DEFAULT_PORT,
            'persistent' => self::DEFAULT_PERSISTENT,
            'weight'  => self::DEFAULT_WEIGHT,
            'timeout' => self::DEFAULT_TIMEOUT,
            'retry_interval' => self::DEFAULT_RETRY_INTERVAL,
            'status' => self::DEFAULT_STATUS,
            'failure_callback' => self::DEFAULT_FAILURE_CALLBACK
	)),
        'compression' => false,
        'compatibility' => false,
	);

	/**
	 * Memcache object
	 *
	 * @var mixed memcache object
	 */
	protected $_memcache = null;

	/**
	 * Constructor
	 *
	 * @param array $options associative array of options
	 * @throws Zend_Cache_Exception
	 * @return void
	 */
	public function __construct(array $options = array())
	{
		if (!extension_loaded('memcache')) {
			Zend_Cache::throwException('The memcache extension must be loaded for using this backend !');
		}
		parent::__construct($options);
		if (isset($this->_options['servers'])) {
			$value= $this->_options['servers'];
			if (isset($value['host'])) {
				// in this case, $value seems to be a simple associative array (one server only)
				$value = array(0 => $value); // let's transform it into a classical array of associative arrays
			}
			$this->setOption('servers', $value);
		}
		$this->_memcache = new Memcache;
		foreach ($this->_options['servers'] as $server) {
			if (!array_key_exists('port', $server)) {
				$server['port'] = self::DEFAULT_PORT;
			}
			if (!array_key_exists('persistent', $server)) {
				$server['persistent'] = self::DEFAULT_PERSISTENT;
			}
			if (!array_key_exists('weight', $server)) {
				$server['weight'] = self::DEFAULT_WEIGHT;
			}
			if (!array_key_exists('timeout', $server)) {
				$server['timeout'] = self::DEFAULT_TIMEOUT;
			}
			if (!array_key_exists('retry_interval', $server)) {
				$server['retry_interval'] = self::DEFAULT_RETRY_INTERVAL;
			}
			if (!array_key_exists('status', $server)) {
				$server['status'] = self::DEFAULT_STATUS;
			}
			if (!array_key_exists('failure_callback', $server)) {
				$server['failure_callback'] = self::DEFAULT_FAILURE_CALLBACK;
			}
			if ($this->_options['compatibility']) {
				// No status for compatibility mode (#ZF-5887)
				$this->_memcache->addServer($server['host'], $server['port'], $server['persistent'],
				$server['weight'], $server['timeout'],
				$server['retry_interval']);
			} else {
				$this->_memcache->addServer($server['host'], $server['port'], $server['persistent'],
				$server['weight'], $server['timeout'],
				$server['retry_interval'],
				$server['status'], $server['failure_callback']);
			}
		}
	}

	/**
	 * Test if a cache is available for the given id and (if yes) return it (false else)
	 *
	 * @param  string  $id                     Cache id
	 * @param  boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested
	 * @return string|false cached datas
	 */
	public function load($id, $doNotTestCacheValidity = false)
	{
		$tmp = $this->_memcache->get($id);
		if (is_array($tmp)) {
			return $tmp[0];
		}
		return false;
	}

	/**
	 * Test if a cache is available or not (for the given id)
	 *
	 * @param  string $id Cache id
	 * @return mixed|false (a cache is not available) or "last modified" timestamp (int) of the available cache record
	 */
	public function test($id)
	{
		$tmp = $this->_memcache->get($id);
		if (is_array($tmp)) {
			return $tmp[1];
		}
		return false;
	}

	/**
	 * Save some string datas into a cache record
	 *
	 * Note : $data is always "string" (serialization is done by the
	 * core not by the backend)
	 *
	 * @param  string $data             Datas to cache
	 * @param  string $id               Cache id
	 * @param  array  $tags             Array of strings, the cache record will be tagged by each string entry
	 * @param  int    $specificLifetime If != false, set a specific lifetime for this cache record (null => infinite lifetime)
	 * @return boolean True if no problem
	 */
	public function save($data, $id, $tags = array(), $specificLifetime = false)
	{
		$lifetime = $this->getLifetime($specificLifetime);
		if ($this->_options['compression']) {
			$flag = MEMCACHE_COMPRESSED;
		} else {
			$flag = 0;
		}
		// #ZF-5702 : we try add() first becase set() seems to be slower
		if (!($result = $this->_memcache->add($id, array($data, time(), $lifetime), $flag, $lifetime))) {
			$result = $this->_memcache->set($id, array($data, time(), $lifetime), $flag, $lifetime);
		}
		if (count($tags) > 0) {
			$this->_log("Zend_Cache_Backend_Memcached::save() : tags are unsupported by the Memcached backend");
		}
		return $result;
	}

	/**
	 * Remove a cache record
	 *
	 * @param  string $id Cache id
	 * @return boolean True if no problem
	 */
	public function remove($id)
	{
		return $this->_memcache->delete($id);
	}

	/**
	 * Clean some cache records
	 *
	 * Available modes are :
	 * 'all' (default)  => remove all cache entries ($tags is not used)
	 * 'old'            => unsupported
	 * 'matchingTag'    => unsupported
	 * 'notMatchingTag' => unsupported
	 * 'matchingAnyTag' => unsupported
	 *
	 * @param  string $mode Clean mode
	 * @param  array  $tags Array of tags
	 * @throws Zend_Cache_Exception
	 * @return boolean True if no problem
	 */
	public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array())
	{
		switch ($mode) {
			case Zend_Cache::CLEANING_MODE_ALL:
				return $this->_memcache->flush();
				break;
			case Zend_Cache::CLEANING_MODE_OLD:
				$this->_log("Zend_Cache_Backend_Memcached::clean() : CLEANING_MODE_OLD is unsupported by the Memcached backend");
				break;
			case Zend_Cache::CLEANING_MODE_MATCHING_TAG:
			case Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG:
			case Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG:
				$this->_log(self::TAGS_UNSUPPORTED_BY_CLEAN_OF_MEMCACHED_BACKEND);
				break;
			default:
				Zend_Cache::throwException('Invalid mode for clean() method');
				break;
		}
	}

	/**
	 * Return true if the automatic cleaning is available for the backend
	 *
	 * @return boolean
	 */
	public function isAutomaticCleaningAvailable()
	{
		return false;
	}

	/**
	 * Set the frontend directives
	 *
	 * @param  array $directives Assoc of directives
	 * @throws Zend_Cache_Exception
	 * @return void
	 */
	public function setDirectives($directives)
	{
		parent::setDirectives($directives);
		$lifetime = $this->getLifetime(false);
		if ($lifetime > 2592000) {
			// #ZF-3490 : For the memcached backend, there is a lifetime limit of 30 days (2592000 seconds)
			$this->_log('memcached backend has a limit of 30 days (2592000 seconds) for the lifetime');
		}
		if ($lifetime === null) {
			// #ZF-4614 : we tranform null to zero to get the maximal lifetime
			parent::setDirectives(array('lifetime' => 0));
		}
	}

	/**
	 * Return an array of stored cache ids
	 *
	 * @return array array of stored cache ids (string)
	 */
	public function getIds()
	{
		$this->_log("Zend_Cache_Backend_Memcached::save() : getting the list of cache ids is unsupported by the Memcache backend");
		return array();
	}

	/**
	 * Return an array of stored tags
	 *
	 * @return array array of stored tags (string)
	 */
	public function getTags()
	{
		$this->_log(self::TAGS_UNSUPPORTED_BY_SAVE_OF_MEMCACHED_BACKEND);
		return array();
	}

	/**
	 * Return an array of stored cache ids which match given tags
	 *
	 * In case of multiple tags, a logical AND is made between tags
	 *
	 * @param array $tags array of tags
	 * @return array array of matching cache ids (string)
	 */
	public function getIdsMatchingTags($tags = array())
	{
		$this->_log(self::TAGS_UNSUPPORTED_BY_SAVE_OF_MEMCACHED_BACKEND);
		return array();
	}

	/**
	 * Return an array of stored cache ids which don't match given tags
	 *
	 * In case of multiple tags, a logical OR is made between tags
	 *
	 * @param array $tags array of tags
	 * @return array array of not matching cache ids (string)
	 */
	public function getIdsNotMatchingTags($tags = array())
	{
		$this->_log(self::TAGS_UNSUPPORTED_BY_SAVE_OF_MEMCACHED_BACKEND);
		return array();
	}

	/**
	 * Return an array of stored cache ids which match any given tags
	 *
	 * In case of multiple tags, a logical AND is made between tags
	 *
	 * @param array $tags array of tags
	 * @return array array of any matching cache ids (string)
	 */
	public function getIdsMatchingAnyTags($tags = array())
	{
		$this->_log(self::TAGS_UNSUPPORTED_BY_SAVE_OF_MEMCACHED_BACKEND);
		return array();
	}

	/**
	 * Return the filling percentage of the backend storage
	 *
	 * @throws Zend_Cache_Exception
	 * @return int integer between 0 and 100
	 */
	public function getFillingPercentage()
	{
		$mems = $this->_memcache->getExtendedStats();

		$memSize = 0;
		$memUsed = 0;
		foreach ($mems as $key => $mem) {
			if ($mem === false) {
				Zend_Cache::throwException('can\'t get stat from ' . $key);
			} else {
				$eachSize = $mem['limit_maxbytes'];
				if ($eachSize == 0) {
					Zend_Cache::throwException('can\'t get memory size from ' . $key);
				}

				$eachUsed = $mem['bytes'];
				if ($eachUsed > $eachSize) {
					$eachUsed = $eachSize;
				}

				$memSize += $eachSize;
				$memUsed += $eachUsed;
			}
		}

		return ((int) (100. * ($memUsed / $memSize)));
	}

	/**
	 * Return an array of metadatas for the given cache id
	 *
	 * The array must include these keys :
	 * - expire : the expire timestamp
	 * - tags : a string array of tags
	 * - mtime : timestamp of last modification time
	 *
	 * @param string $id cache id
	 * @return array array of metadatas (false if the cache id is not found)
	 */
	public function getMetadatas($id)
	{
		$tmp = $this->_memcache->get($id);
		if (is_array($tmp)) {
			$data = $tmp[0];
			$mtime = $tmp[1];
			if (!isset($tmp[2])) {
				// because this record is only with 1.7 release
				// if old cache records are still there...
				return false;
			}
			$lifetime = $tmp[2];
			return array(
                'expire' => $mtime + $lifetime,
                'tags' => array(),
                'mtime' => $mtime
			);
		}
		return false;
	}

	/**
	 * Give (if possible) an extra lifetime to the given cache id
	 *
	 * @param string $id cache id
	 * @param int $extraLifetime
	 * @return boolean true if ok
	 */
	public function touch($id, $extraLifetime)
	{
		if ($this->_options['compression']) {
			$flag = MEMCACHE_COMPRESSED;
		} else {
			$flag = 0;
		}
		$tmp = $this->_memcache->get($id);
		if (is_array($tmp)) {
			$data = $tmp[0];
			$mtime = $tmp[1];
			if (!isset($tmp[2])) {
				// because this record is only with 1.7 release
				// if old cache records are still there...
				return false;
			}
			$lifetime = $tmp[2];
			$newLifetime = $lifetime - (time() - $mtime) + $extraLifetime;
			if ($newLifetime <=0) {
				return false;
			}
			// #ZF-5702 : we try replace() first becase set() seems to be slower
			if (!($result = $this->_memcache->replace($id, array($data, time(), $newLifetime), $flag, $newLifetime))) {
				$result = $this->_memcache->set($id, array($data, time(), $newLifetime), $flag, $newLifetime);
			}
			return $result;
		}
		return false;
	}

	/**
	 * Return an associative array of capabilities (booleans) of the backend
	 *
	 * The array must include these keys :
	 * - automatic_cleaning (is automating cleaning necessary)
	 * - tags (are tags supported)
	 * - expired_read (is it possible to read expired cache records
	 *                 (for doNotTestCacheValidity option for example))
	 * - priority does the backend deal with priority when saving
	 * - infinite_lifetime (is infinite lifetime can work with this backend)
	 * - get_list (is it possible to get the list of cache ids and the complete list of tags)
	 *
	 * @return array associative of with capabilities
	 */
	public function getCapabilities()
	{
		return array(
            'automatic_cleaning' => false,
            'tags' => false,
            'expired_read' => false,
            'priority' => false,
            'infinite_lifetime' => false,
            'get_list' => false
		);
	}

}
